package controller.webservice.bootstrap;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;

import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import au.com.bytecode.opencsv.CSVReader;
import exception.InputValidationErrors;

/**
 * An abstract class that exposes methods for reading, validating and loading
 * CSV files.
 * 
 * @author jaslin.wee.2012, huangjm.2012, sheryl.tay.2012
 * 
 */
public abstract class CsvLoader {

	private static final String FIELD_IS_BLANK_POSTFIX_WITH_WHITESPACE = " is blank";
	private CSVReader csvReader;
	private String[] currentRow;
	private int currentRowNo = 0;
	private String[] fieldNames;
	private String file;
	private int numRowsLoaded = 0;
	private ArrayList<JSONObject> rowErrors = new ArrayList<JSONObject>();

	/**
	 * Constructor opens the CSV file for reading and reads the header line.
	 * 
	 * @param pathToFile
	 *            path to the CSV file, e.g. /path/to/data/student.csv
	 * @throws IOException
	 *             if the given file is not found or an error occurs while
	 *             reading the header line
	 */
	CsvLoader(String pathToFile) throws IOException {
		setFile(pathToFile);

		setCsvReaderForFile(pathToFile);

		setFieldNames();
	}

	/**
	 * Checks for blank fields in the row.
	 * 
	 * @throws InputValidationErrors
	 *             if there are blank fields
	 */
	private void checkForBlankFieldsInRow() throws InputValidationErrors {
		ArrayList<String> blankFieldErrors = new ArrayList<String>();

		for (int i = 0; i < currentRow.length; i++) {
			String eachField = currentRow[i].trim();
			if (eachField == null || eachField.isEmpty()) {
				blankFieldErrors.add(fieldNames[i] + FIELD_IS_BLANK_POSTFIX_WITH_WHITESPACE);
			}
		}

		if (!blankFieldErrors.isEmpty()) {
			throw new InputValidationErrors(blankFieldErrors);
		}
	}

	/**
	 * Returns the field number, 0,1,2,.., etc corresponding to the given
	 * field/header name.
	 * 
	 * @param fieldName
	 * @return the corresponding field number
	 */
	private int getFieldNo(String fieldName) {
		for (int i = 0; i < fieldNames.length; i++) {
			if (fieldNames[i].equals(fieldName)) {
				return i;
			}
		}
		return -1;
	}

	/**
	 * Logs error messages for the current row.
	 * 
	 * @param messages
	 *            a JSON Array containing the error messages for the row
	 */
	private void logRowErrorMessages(JSONArray messages) {
		JSONObject errorObj = new JSONObject();
		errorObj.put("file", file);
		errorObj.put("line", currentRowNo);
		errorObj.put("message", messages);
		rowErrors.add(errorObj);
	}

	/**
	 * Opens the CSV file for reading and sets the CSV reader.
	 * 
	 * @param pathToFile
	 * @throws FileNotFoundException
	 */
	private void setCsvReaderForFile(String pathToFile)
			throws FileNotFoundException {
		csvReader = new CSVReader(new FileReader(pathToFile));
	}

	/**
	 * Reads the header line and sets the field/header names.
	 * 
	 * @throws IOException
	 */
	private void setFieldNames() throws IOException {
		fieldNames = csvReader.readNext();
		currentRowNo++;
	}

	/**
	 * Set the CSV file name.
	 * 
	 * @param pathToFile
	 *            full file path of CSV file
	 */
	private void setFile(String pathToFile) {
		file = new File(pathToFile).getName();
	}

	/**
	 * Returns the CSV file name, e.g. student.csv.
	 * 
	 * @return CSV file name
	 */
	String getFile() {
		return file;
	}

	/**
	 * Returns the number of rows that were successfully loaded into the
	 * database.
	 * 
	 * @return number of rows
	 */
	int getNumRowsLoaded() {
		return numRowsLoaded;
	}

	/**
	 * Returns the error messages logged while loading the CSV file.
	 * 
	 * @return an array list of JSON objects containing the error messages for
	 *         each erroneous lines in the CSV file
	 */
	ArrayList<JSONObject> getRowErrors() {
		return rowErrors;
	}

	/**
	 * Returns the value in the current row for the given field name. Uses the
	 * {@link java.lang.String#trim()} to trim white spaces surrounding the
	 * value.
	 * 
	 * @param fieldName
	 *            the column header/field name that can be found in the CSV
	 *            file, e.g. userid, code, etc
	 * @return the value in the current row for the given field name in string
	 *         type
	 */
	String getValue(String fieldName) {
		return currentRow[getFieldNo(fieldName)].trim();
	}

	/**
	 * Starts loading the rows in the CSV file, line-by-line. Logs any
	 * validation errors encountered while loading each row.
	 */
	void load() {
		try {
			while ((currentRow = csvReader.readNext()) != null) {
				currentRowNo++;
				try {
					checkForBlankFieldsInRow();

					if (loadRow()) {
						numRowsLoaded++;
					}

				} catch (InputValidationErrors e) {
					logRowErrorMessages(e.toJsonArray());
				}
			}

			csvReader.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Loads a single row of the CSV file into the database, after performing
	 * input/logical validations. Does not loads row if there are validation
	 * errors.
	 * 
	 * @return true if the row is successfully loaded into database, false
	 *         otherwise
	 * @throws InputValidationErrors
	 *             if there are validation errors
	 */
	abstract boolean loadRow() throws InputValidationErrors;

}
